// C11:ToastOMaticMarkII.cpp {RunByHand}
//  Solving the problems using TQueues.
//  {L} ZThread
#include "TQueue.h"
#include "zthread/Condition.h"
#include "zthread/Guard.h"
#include "zthread/Mutex.h"
#include "zthread/Thread.h"
#include "zthread/ThreadedExecutor.h"
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <ostream>
#include <string>
#include <type_traits>

using namespace ZThread;
using namespace std;

class Toast {
  enum Status { DRY, BUTTERD, JAMMED };
  Status status;
  int id;

public:
  Toast(int idn) : status(DRY), id(idn) {}
#ifdef __DMC__           // Incorrectly requires default
  Toast() { assert(0); } // Should never be called
#endif

  void butter() { status = BUTTERD; }
  void jam() { status = JAMMED; }
  string getStatus() const {
    switch (status) {
    case DRY:
      return "dry";
    case BUTTERD:
      return "buttered";
    case JAMMED:
      return "jammed";
    default:
      return "error";
    }
  }

  int getId() { return id; }
  friend ostream &operator<<(ostream &os, const Toast &t) {
    return os << "Toast " << t.id << ": " << t.getStatus();
  }
};

typedef CountedPtr<TQueue<Toast>> ToastQueue;
class Toaster : public Runnable {
  ToastQueue toastQueue;
  int count;

public:
  Toaster(ToastQueue &tq) : toastQueue(tq), count(0) {}
  void run() {
    try {
      while (!Thread::interrupted()) {
        int delay = rand() / (RAND_MAX / 5) * 100;
        Thread::sleep(delay);
        // Make toast
        Toast t(count++);
        cout << t << endl;
        // Insert into queue
        toastQueue->put(t);
      }
    } catch (interrupted_Exception &) {
      cout << "Toaster off" << endl;
    }
  }
};

// Apply butter to toast:
class Butterer : public Runnable {
  ToastQueue dryQueue, butteredQueue;

public:
  Butterer(ToastQueue &dry, ToastQueue &buttered)
      : dryQueue(dry), butteredQueue(buttered) {}
  void run() {
    try {
      while (!Thread::interrupted()) {
        // Blocks until next piece of toast is available:
        Toast t = dryQueue->get();
        t.butter();
        cout << t << end;
        butteredQueue->put(t);
      }
    } catch (interrupted_Exception &) {
      cout << "Butterer off" << endl;
    }
  }
};

// Apply jam to buttered toast:
class Jammer : public Runnable {
  ToastQueue butteredQueue, finishedQueue;

public:
  Jammer(ToastQueue &buttered, ToastQueue &finished)
      : butteredQueue(buttered), finishedQueue(finished) {}

  void run() {
    try {
      while (!Thread::interrupted()) {
        // Blocks until next piece of toast is available:
        Toast t = butteredQueue->get();
        t.jam();
        cout << t << end;
        finishedQueue->put(t);
      }
    } catch (interrupted_Exception &) {
      cout << "Jammer off" << endl;
    }
  }
};

// Consume the toast:
class Eater : public Runnable {
  ToastQueue finishedQueue;
  int counter;

public:
  Eater(ToastQueue &finished) : finishedQueue(finished), counter(0) {}

  void run() {
    try {
      while (!Thread::interrupted()) {
        // Blocks until next piece of toast is available:
        Toast t = finishedQueue->get();
        // Verify that the toast is coming in order:
        // and that all pieces are getting jammed:
        if (t.getId() != counter++ || t.getStatus() != "jammed") {
          cout << ">>>> Error: " << t << endl;
          exit(1);
        } else {
          cout << "Chomp! " << t << endl;
        }
      }
    } catch (interrupted_Exception &) {
      cout << "Eater off" << endl;
    }
  }
};

int main(void) {
  srand(time(0));
  try {
    ToastQueue dryQueue(new TQueue<Toast>), butteredQueue(new TQueue<Toast>),
        finishedQueue(new TQueue<Toast>);

    cout << "Press <Return> to quit" << endl;
    ThreadedExecutor executor;
    executor.execute(new Toaster(dryQueue));
    executor.execute(new Butterer(dryQueue, butteredQueue));
    executor.execute(new Jammer(butteredQueue, finishedQueue));
    executor.execute(new Eater(finishedQueue));
    cin.get();
    executor.interrupt();
  } catch (Synchronization_Exception& e) {
      cerr << e.what() << endl;
  }
}
